#include "Parser.h"

//Counts the number of special character delimiters or '/' found in the string.
//Throws exceptions if the number does not tally with the number for any task.
//Returns a TaskType depending on the number of delimiters found.
Task::TaskType Parser::determineTask(QString parameters) {
    int determineType = 0, position = 0;

    do {
        position = parameters.indexOf(DELIMITER);
        if (position != string::npos) {
            determineType++;
            position++;
            parameters = parameters.right(position);
        }
    } while (position != string::npos);

    switch(determineType) {
        case 0: return Task::TaskType::FLOATING;
        case 2: return Task::TaskType::DEADLINE;
        case 3: return Task::TaskType::MEETING;
        default:
            throw InvalidInputException("unrecognized error. ");
    };
}

//Extracts the entire parameter if the TaskType is FLOATING. Finds the first 
//delimiter in string and extracts content before delimiter and trims the 
//whitespaces after the actual content.

//Notes:
//1. Used for the parsing adding task inupt variables. 
//2.Takes in a TaskType and a string reference to the original parameters.
//The parameters that have been extracted will be trimmed from the original
//parameter string.
QString Parser::retrieveParam(Task::TaskType taskType, QString &parameters) {
    QString param;

    if (taskType != Task::TaskType::FLOATING) {
        int position = parameters.indexOf(DELIMITER);
        assert(position != string::npos);

		param = parameters.left(position);
		util.removeWhiteSpace(param);

        if (param.isEmpty()) {
            throw InvalidInputException("no input parameters detected, please enter valid parameters. ");
        }

        return param;
    }
    else {
        param = parameters;
        parameters.clear();
        return param;
    }
}

//Extracts parameters during the situation where delimiters are whitespaces as
//opposed to special charaters in the addCommand. Throws exceptions when:

//1. string is empty
//2. insufficient task/command parameters
//3. missing components/modifying details.
void Parser::retrieveParamModify(QString &parameters, QString &component, QString &modifyDetails) {
    QString param = parameters;

	if (parameters.isEmpty()) {
		throw InvalidInputException("no input parameters detected, please enter valid parameters. ");
	}

    vector<string> modifyTask = util.splitParameters(parameters.toStdString());
	
	if (modifyTask.size() < 3) {
		throw InvalidInputException("insufficient command/task parameters. Please try again. ");
	}

	int index;

	for (index = 0; index < modifyTask.size(); index++) {
		QString temp = QString::fromStdString(modifyTask[index]);
		if ((util.equalsIgnoreCase(temp, "title")) ||
			(util.equalsIgnoreCase(temp, "startdate")) ||
			(util.equalsIgnoreCase(temp, "enddate")) ||
			(util.equalsIgnoreCase(temp, "starttime")) ||
			(util.equalsIgnoreCase(temp, "endtime")) ||
			(util.equalsIgnoreCase(temp, "tags"))) {
				component.fromStdString(modifyTask[index]);
				break;
		}
	}

	if (index == modifyTask.size()) {
		throw InvalidInputException("missing component, please enter component to be modified. ");
	}

	if ((index + 1) == modifyTask.size()) {
		throw InvalidInputException("missing task parameters, please enter task parameters. ");
	}

	for (index = index + 1; index < modifyTask.size(); index++) {
		modifyDetails = modifyDetails + QString::fromStdString(modifyTask[index]) + " ";
	}

	if (modifyDetails.isEmpty()) {
		throw InvalidInputException("missing task parameters, please enter task parameters. ");
	}
}

//Interprets user input by matching it with regular expression patterns
//constructed to interpret dates. If there is an exact match, the
//relevant parameters are extracted and parsed to return a date class.
//Throws an exception if the date format is unrecognized.

//Supports the following date expression formats, taking date as 4th April.
//1. All Digits: 0404
//2. Long Months: 04 April, 4th April, 04th April
//3. Short Months: 04 Apr, 4th Apr, 04th Apr
//4. Long Weekdays: next Tuesday, coming Tuesday
//5. Short Weekdays: next Tue, coming Tue
//6. Day References: today, yesterday, tomorrow, day after tomorrow, next week
QDate Parser::parseDate(QString parameters) {
    QRegExp emptyStr(RX_EMPTY_STRING, Qt::CaseInsensitive);
    QRegExp allDigits(RX_ALL_DIGITS, Qt::CaseInsensitive);
    QRegExp longMonth(RX_LONG_MONTH, Qt::CaseInsensitive);
    QRegExp shortMonth(RX_SHORT_MONTH, Qt::CaseInsensitive);
    QRegExp longWeekdays(RX_LONG_WEEKDAYS, Qt::CaseInsensitive);
    QRegExp shortWeekdays(RX_SHORT_WEEKDAYS, Qt::CaseInsensitive);
    QRegExp today(RX_TODAY, Qt::CaseInsensitive);
    QRegExp tomorrow(RX_TOMORROW, Qt::CaseInsensitive);
    QRegExp yesterday(RX_YESTERDAY, Qt::CaseInsensitive);
    QRegExp dayAfter(RX_DAY_AFTER, Qt::CaseInsensitive);
    QRegExp nextWeek(RX_NEXT_WEEK, Qt::CaseInsensitive);

    QString userInput = parameters;
    QString getParam1, getParam2;

    if (emptyStr.exactMatch(userInput)) {
        throw InvalidInputException("no input parameters detected, please enter a valid date. ");
    }

    if (allDigits.exactMatch(userInput)) {
        getParam1 = allDigits.cap(1);
        return parseDigitsDate(getParam1);
    }

    if (longMonth.exactMatch(userInput)) {
        getParam1 = longMonth.cap(1);
        getParam2 = longMonth.cap(2);
        return parseLongShortMonthDate(getParam1, getParam2);
    }

    if (shortMonth.exactMatch(userInput)) {
        getParam1 = shortMonth.cap(1);
        getParam2 = shortMonth.cap(2);
        return parseLongShortMonthDate(getParam1, getParam2);
    }

    if (longWeekdays.exactMatch(userInput)) {
        getParam1 = longWeekdays.cap(1);
        return parseLongShortWeekday(getParam1);
    }

    if (shortWeekdays.exactMatch(userInput)) {
        getParam1 = shortWeekdays.cap(1);
        return parseLongShortWeekday(getParam1);
    }

    if (today.exactMatch(userInput)) {
        return parseToday();
    }

    if (tomorrow.exactMatch(userInput)) {
        return parseTomorrow();
    }

    if (yesterday.exactMatch(userInput)) {
        return parseYesterday();
    }

    if (dayAfter.exactMatch(userInput)) {
        return parseDayAfter();
    }

    if (nextWeek.exactMatch(userInput)) {
        return parseNextWeek();
    }

    throw InvalidInputException("unrecognized date format, please enter date in a valid format. ");
}

//Interprets user input by matching it with regular expression patterns
//constructed to interpret time. If there is an exact match, the
//relevant parameters are extracted and parsed to return a time class.
//Throws an exception if the time format is unrecognized.

//Supports the following time expression formats, taking date as 7pm.
//1. All Digits: 1900
//2. AM/PM Time: 7:00pm, 7pm, 07:00pm
//3. 24 Hour Time: 19:00

//Notes:
//1. Includes a special regular expression to parse time classes ending with a
//special character. This regexp is used for parsing time classes when loading
//the session.
QTime Parser::parseTime(QString parameters) {
    QRegExp emptyStr(RX_EMPTY_STRING, Qt::CaseInsensitive);
    QRegExp allDigits(RX_ALL_DIGITS, Qt::CaseInsensitive);
    QRegExp timeAllDigits(RX_TIME_ALL_DIGITS, Qt::CaseInsensitive);
    QRegExp APTime(RX_AP_TIME, Qt::CaseInsensitive);
    QRegExp hourTime(RX_24_HOUR_TIME, Qt::CaseInsensitive);
    QRegExp APTime2Digits(RX_AP_TIME_2_DIGITS, Qt::CaseInsensitive);

    QString userInput = parameters;
    QString getParam1, getParam2, getParam3;

    if (emptyStr.exactMatch(userInput)) {
        throw InvalidInputException("no input parameters detected, please enter a valid time. ");
    }

    if (allDigits.exactMatch(userInput)) {
        getParam1 = allDigits.cap(1);
        return parseDigitsTime(getParam1);
    }

	//This function is used to recognise time formats with a special character 
	//separated with a space set during saveSession.
    if (timeAllDigits.exactMatch(userInput)) {
        getParam1 = timeAllDigits.cap(1);
        return parseDigitsTime(getParam1);
    }

    if (APTime.exactMatch(userInput)) {
        getParam1 = APTime.cap(1);
        getParam2 = APTime.cap(2);
        getParam3 = APTime.cap(3);
        return parseAPTime(getParam1, getParam2, getParam3);
    }

    if (APTime2Digits.exactMatch(userInput)) {
        getParam1 = APTime2Digits.cap(1);
        getParam2 = APTime2Digits.cap(2);
        return parseAPTime2Digits(getParam1, getParam2);
    }

    if (hourTime.exactMatch(userInput)) {
        getParam1 = hourTime.cap(1);
        getParam2 = hourTime.cap(2);
        return parse24HourTime(getParam1, getParam2);
    }

    throw InvalidInputException("unrecognisable time format, please enter time in a valid format. ");
}

//This function is used mainly during session loading.
//Returns true if the character representing complete is found in the string.
bool Parser::parseComplete(QString &parameters) {
    if (parameters.contains(COMPLETE_CHAR)){
        return true;
    }

    return false;
}

//This function parses dates in the form of digits. Returns a date class
//if the date is valid.
QDate Parser::parseDigitsDate(QString allDigits) {
    int setDay, setMonth;
    util.extractDigits(allDigits, setDay, setMonth);

    QDate checkDate;
    if (checkDate.setDate(2014, setMonth, setDay)) {
		return checkDate;
	}
	else {
		throw InvalidInputException("date is not valid, please input a valid date");
	}
}

//This function parses dates in the form of months. Returns a date class
//if the date is valid.
QDate Parser::parseLongShortMonthDate(QString digit, QString month) {
    int setDay, setMonth;

    util.extractDigits(digit, setDay);
    setMonth = retrieveMonth(month);

    QDate checkDate;
    if (checkDate.setDate(2014, setMonth, setDay)) {
		return checkDate;
	}
	else {
		throw InvalidInputException("date is not valid, please input a valid date");
	}
}

//This function parses dates in the form of weekdays. It parses 2 cases:
//1. When the current date is smaller than the weekday in question, the function 
//sets the day to Sunday and adds the remaining dates.
//2. When the current date greater than the weekday in question, the function returns 
//the first weekday that occurs after Sunday.

//Returns a date class if the date is valid.
QDate Parser::parseLongShortWeekday(QString weekday) {
    QDate currDate = QDate::currentDate();
	int currWeekDay = currDate.dayOfWeek();
    int numDays = retrieveWeekday(weekday);

    if (currWeekDay <= numDays) {
        for (int i = 1; i <= (SUNDAY - currWeekDay); i++) {
			currDate = currDate.addDays(1);
		}
    }
	
	for (int i = 1; i <= 7; i++) {
		currDate = currDate.addDays(1);

        if ((util.equalsIgnoreCase(currDate.toString("ddd"), weekday)) ||
			(util.equalsIgnoreCase(currDate.toString("dddd"), weekday))) {
				break;
		}
	}
	
	return currDate;
}

//This function parses dates expressed as today. Returns a date class
//if the date is valid.
QDate Parser::parseToday() {
    return QDate::currentDate();
}

//This function parses dates expressed as tomorrow. Returns a date class
//if the date is valid.
QDate Parser::parseTomorrow() {
    QDate tomorrow = QDate::currentDate();
    tomorrow = tomorrow.addDays(1);

    return tomorrow;
}

//This function parses dates expressed as yesterday. Returns a date class
//if the date is valid.
QDate Parser::parseYesterday() {
    QDate yesterday = QDate::currentDate();
    yesterday = yesterday.addDays(-1);

	return yesterday;
}

//This function parses dates expressed as day after tomorrow. Returns a date class
//if the date is valid.
QDate Parser::parseDayAfter() {
    QDate dayAfter = QDate::currentDate();
    dayAfter = dayAfter.addDays(2);

    return dayAfter;
}

//This function parses dates expressed as next week. Returns a date class
//if the date is valid.
QDate Parser::parseNextWeek() {
    QDate nextWeek = QDate::currentDate();
    nextWeek = nextWeek.addDays(7);

    return nextWeek;
}

//This function parses times in the form of digits. Returns a time class
//if the time is valid.
QTime Parser::parseDigitsTime(QString allDigits) {
    int setHour, setMinute;
    util.extractDigits(allDigits, setHour, setMinute);

    QTime checkTime;
    if (checkTime.setHMS(setHour, setMinute, 0)) {
		return checkTime;
	}
	else {
		throw InvalidInputException("time is not valid, please input a valid time");
	}
}

//This function parses times in the form AM/PM with 4 digits. Returns a time class
//if the time is valid.
QTime Parser::parseAPTime(QString hour, QString minute, QString AMOrPM) {
    int setHour, setMinute;
    QTime checkTime;

    util.extractDigits(hour, setHour);
    util.extractDigits(minute, setMinute);

    if (util.equalsIgnoreCase(AMOrPM, PM)) {
        setHour += 12;
    }
	
	if (checkTime.setHMS(setHour, setMinute, 0)) {
		return checkTime;
	}
	else {
		throw InvalidInputException("time is not valid, please input a valid time");
	}
}

//This function parses times in the form of AM/PM with 2 digits. Returns a time class
//if the time is valid.
QTime Parser::parseAPTime2Digits(QString hour, QString AMOrPM) {
    int setHour, setMinute = 0;
    QTime checkTime;

    util.extractDigits(hour, setHour);

    if (util.equalsIgnoreCase(AMOrPM, PM)) {
        setHour += 12;
      
    }
	
	if (checkTime.setHMS(setHour, setMinute, 0)) {
		return checkTime;
	}
	else {
		throw InvalidInputException("time is not valid, please input a valid time");
	}
}

//This function parses times in the form 24 hour time with a colon in the middle. 
//Returns a time class if the time is valid.
QTime Parser::parse24HourTime(QString hour, QString minute) {
    int setHour, setMinute;
    QTime checkTime;

    util.extractDigits(hour, setHour);
    util.extractDigits(minute, setMinute);

    if (checkTime.setHMS(setHour, setMinute, 0)) {
		return checkTime;
	}
	else {
		throw InvalidInputException("time is not valid, please input a valid time");
	}
}

//This function matches long/short months with the given month string.
//Returns the months as an integer.
int Parser::retrieveMonth(QString month) {
    if (util.equalsIgnoreCase(month, LONG_JAN) || util.equalsIgnoreCase(month, SHORT_JAN)) {
        return JANUARY;
    }

    if (util.equalsIgnoreCase(month, LONG_FEB) || util.equalsIgnoreCase(month, SHORT_FEB)) {
        return FEBRUARY;
    }

    if (util.equalsIgnoreCase(month, LONG_MAR) || util.equalsIgnoreCase(month, SHORT_MAR)) {
        return MARCH;
    }

    if (util.equalsIgnoreCase(month, LONG_APR) || util.equalsIgnoreCase(month, SHORT_APR)) {
        return APRIL;
    }

    if (util.equalsIgnoreCase(month, LONG_MAY) || util.equalsIgnoreCase(month, SHORT_MAY)) {
        return MAY;
    }

    if (util.equalsIgnoreCase(month, LONG_JUN) || util.equalsIgnoreCase(month, SHORT_JUN)) {
        return JUNE;
    }

    if (util.equalsIgnoreCase(month, LONG_JUL) || util.equalsIgnoreCase(month, SHORT_JUL)) {
        return JULY;
    }

    if (util.equalsIgnoreCase(month, LONG_AUG) || util.equalsIgnoreCase(month, SHORT_AUG)) {
        return AUGUST;
    }

    if (util.equalsIgnoreCase(month, LONG_SEP) || util.equalsIgnoreCase(month, SHORT_SEP)) {
        return SEPTEMBER;
    }

    if (util.equalsIgnoreCase(month, LONG_OCT) || util.equalsIgnoreCase(month, SHORT_OCT)) {
        return OCTOBER;
    }

    if (util.equalsIgnoreCase(month, LONG_NOV) || util.equalsIgnoreCase(month, SHORT_NOV)) {
        return NOVEMBER;
    }

    if (util.equalsIgnoreCase(month, LONG_DEC) || util.equalsIgnoreCase(month, SHORT_DEC)) {
        return DECEMBER;
    }

    throw InvalidInputException("unrecognised error. ");
}

//This function matches long/short weekdays with the given weekday string.
//Returns the weekday as an integer.
int Parser::retrieveWeekday(QString weekday) {
    if (util.equalsIgnoreCase(weekday, LONG_MON) || util.equalsIgnoreCase(weekday, SHORT_MON)) {
        return MONDAY;
    }

    if (util.equalsIgnoreCase(weekday, LONG_TUE) || util.equalsIgnoreCase(weekday, SHORT_TUE)) {
        return TUESDAY;
    }

    if (util.equalsIgnoreCase(weekday, LONG_WED) || util.equalsIgnoreCase(weekday, SHORT_WED)) {
        return WEDNESDAY;
    }

    if (util.equalsIgnoreCase(weekday, LONG_THU) || util.equalsIgnoreCase(weekday, SHORT_THU)) {
        return THURSDAY;
    }

    if (util.equalsIgnoreCase(weekday, LONG_FRI) || util.equalsIgnoreCase(weekday, SHORT_FRI)) {
        return FRIDAY;
    }

    if (util.equalsIgnoreCase(weekday, LONG_SAT) || util.equalsIgnoreCase(weekday, SHORT_SAT)) {
        return SATURDAY;
    }

    if (util.equalsIgnoreCase(weekday, LONG_SUN) || util.equalsIgnoreCase(weekday, SHORT_SUN)) {
        return SUNDAY;
    }

    throw InvalidInputException("unrecognised error. ");
}
